package licode.unisop.oidc.authsa.stp;

import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.listener.SaTokenEventCenter;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.session.TokenSign;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import com.alibaba.fastjson.JSON;
import licode.unisop.oidc.auth.modal.AuthorContext;
import licode.unisop.oidc.auth.modal.OidcToken;
import licode.unisop.oidc.auth.modal.SsoToken;
import licode.unisop.oidc.auth.utils.StringTool;
import licode.unisop.oidc.authsa.service.SaSessionProxy;
import licode.unisop.oidc.authsa.utils.TokenUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * 实现登录逻辑定制
 *
 * @author licode
 */
public class SaStpLogic extends StpLogic {
    public SaStpLogic(String loginType) {
        super(loginType);
    }

    public SaSessionProxy querySessionByLoginId(String loginId) {
        SaSession session = null;
        if (loginId != null) {
            session = getSessionByLoginId(loginId, false);
        }

        if (session instanceof SaSessionProxy) {
            return (SaSessionProxy) session;
        } else {
            return null;
        }
    }

    public void checkLoginStrategy(SaSessionProxy saSession, SaTokenSign tokenSign) {
        if (null != tokenSign) {
            SaTokenConfig config = getConfigOrGlobal();
        }
    }

    public SaSessionProxy login(String loginId, SaLoginModel loginModel,
                                SaTokenSign tokenSign, AuthorContext context) {
        return login(loginId, loginModel, tokenSign, context, null);
    }

    public SaSessionProxy login(String loginId, SaLoginModel loginModel,
                                SaTokenSign tokenSign, AuthorContext context,
                                SaSessionProxy saSession) {
        SaSessionProxy session = saSession;
        Long timeout = loginModel.getTimeout();

        if (null == timeout || timeout <= 0) {
            timeout = getConfigOrGlobal().getTimeout();
        }

        // 1、先检查一下，传入的参数是否有效
        checkLoginArgs(loginId, loginModel);

        // 2、初始化 loginModel ，给一些参数补上默认值
        SaTokenConfig config = configLoginModel(loginModel);

        // 3、获取此账号的 Account-Session , 续期
        if (null == session) {
            session = querySessionByLoginId(loginId);
        }

        if (null == session) {
            session = (SaSessionProxy)getSessionByLoginId(loginId, true);
            session.setToken(buildUsableToken(loginId, loginModel, false));
            session.setUserId(loginModel.getUserId());
            getSaTokenDao().setSession(session, timeout);
        } else {
            checkLoginStrategy(session, tokenSign);
        }

        if (null != tokenSign) {
            // 更新用户会话的超时时间
            session.updateSessionTimeout(timeout, false);
            authLogin(session, loginModel, tokenSign, context);
        }

        // 4、检查此账号会话数量是否超出最大值，如果超过，则按照登录时间顺序，把最开始登录的给注销掉
        if (config.getMaxLoginCount() != -1) {
            logoutByMaxLoginCount(loginId, session, null, config.getMaxLoginCount(), null != tokenSign);
        }

        session.update();

        // 6、一切处理完毕，返回会话凭证 token
        return session;
    }

    /**
     * 对单点登录，生成主会话
     *
     * @param loginId    登录ID（领域+用户ID）
     * @param loginModel 登录参数
     * @return 会话信息
     */
    public SaSessionProxy buildMasterSession(String loginId, SaLoginModel loginModel) {
        String ssoToken;
        SaSessionProxy session;

        session = login(loginId, loginModel, null, null);

        if (null != session && null == session.getSsoToken()) {
            ssoToken = createToken(loginId, "sso_token", "", -1L);
            session.assignSsoToken(ssoToken, loginModel.getTimeout());
            saveSsoTokenToIdMapping(session.getSsoToken().getSsoToken(),
                    session.getLoginId(), session.getTimeout());
            session.update();
        }

        return session;
    }

    public void loginSso(String loginId, SaLoginModel loginModel,
                                   SaTokenSign tokenSign, AuthorContext context) {
        String ssoToken;
        SaTokenSign existTokenSign = null;
        SaSessionProxy session = querySessionByLoginId(loginId);

        if (null != session) {
            existTokenSign = session.findTokenSign(context);
        }

        session = login(loginId, loginModel, tokenSign, context, session);

        if (null == session.getSsoToken() || StringTool.isBlank(session.getSsoToken().getSsoToken())) {
            ssoToken = createToken(loginId, "sso_token", "", -1L);
            session.assignSsoToken(ssoToken, loginModel.getTimeout());
            tokenSign.setSsoToken(ssoToken);
        } else {
            tokenSign.setSsoToken(session.getSsoToken().getSsoToken());
        }

        /*
         * 更新会话和SSO的超时时间
         */
        session.updateSessionTimeout(loginModel.getTimeout(), true);

        SsoToken sessionToken = session.getSsoToken();
        if (null != existTokenSign && existTokenSign.getSsoToken() != null &&
                null != sessionToken && existTokenSign.getSsoToken().equals(sessionToken.getSsoToken())) {
            logoutByAccessToken(existTokenSign.getValue(), session);
        }

        session.update();
    }

    public boolean checkCurrentActiveTimeout() {
        SaTokenConfig cfg = getConfigOrGlobal();
        return cfg.getActiveTimeout() != SaTokenDao.NEVER_EXPIRE || cfg.getDynamicActiveTimeout();
    }

    public void logoutByAccessToken(String accessToken) {
        String loginId = getLoginIdNotHandle(accessToken);
        SaSessionProxy session = querySessionByLoginId(loginId);
        if (null != session) {
            logoutByAccessToken(accessToken, session);
        }
    }

    private boolean checkTokenIsSso(SsoToken ssoToken, List<TokenSign> tokenList, String accessToken) {
        boolean isSsoAccessToken = false;
        for (TokenSign item : tokenList) {
            SaTokenSign saToken = (SaTokenSign) item;
            if (saToken.getValue().equals(accessToken) &&
                    ssoToken.getSsoToken().equals(saToken.getSsoToken())) {
                isSsoAccessToken = true;
                break;
            }
        }
        return isSsoAccessToken;
    }

    public void logoutBySso(String accessToken, SaSessionProxy session) {
        if (session != null) {
            SsoToken ssoToken = session.getSsoToken();
            if (null == ssoToken || null == ssoToken.getSsoToken()) {
                logoutByAccessToken(accessToken, session);
            } else {
                List<TokenSign> tokenList = session.tokenSignListCopy();
                boolean isSsoAccessToken = checkTokenIsSso(ssoToken, tokenList, accessToken);
                if (!isSsoAccessToken) {
                    logoutByAccessToken(accessToken, session);
                } else {
                    for (TokenSign item : tokenList) {
                        SaTokenSign saToken = (SaTokenSign) item;
                        if (ssoToken.getSsoToken().equals(saToken.getSsoToken())) {
                            logoutByAccessToken(saToken.getValue(), session);
                        }
                    }
                }
            }
        }
    }

    public void logoutByAccessToken(String accessToken, SaSessionProxy session) {
        String refreshToken;
        refreshToken = refreshTokenFromAccessToken(accessToken);

        // 删除刷新令牌的映射关系
        if (StringTool.isNotBlank(refreshToken)) {
            this.deleteRefreshTokenToIdMapping(refreshToken);
        }

        // 删除access-info
        deleteAccessInfo(accessToken);

        // 1、清除这个 token 的最后活跃时间记录
        if (checkCurrentActiveTimeout()) {
            clearLastActive(accessToken);
        }

        // 2、清除这个 token 的 Token-Session 对象
        deleteTokenSession(accessToken);

        // 3、清除 token -> id 的映射关系
        String loginId = getLoginIdNotHandle(accessToken);
        if (loginId != null) {
            deleteTokenToIdMapping(accessToken);
        }

        // 4、判断一下：如果此 token 映射的是一个无效 loginId，则此处立即返回，不需要再往下处理了
        if (!isValidLoginId(loginId)) {
            return;
        }

        // 5、$$ 发布事件：某某账号的某某 token 注销下线了
        SaTokenEventCenter.doLogout(loginType, loginId, accessToken);

        // 6、清理这个账号的 Account-Session 上的 token 签名，并且尝试注销掉 Account-Session
        logoutOnSession(session, accessToken);
    }

    public void updateTokenCache(SaTokenSign tokenSign, AuthorContext context, Object loginId) {
        String ctxKey = splicingKeyTokenInfoValue(tokenSign.getValue());
        saveTokenToIdMapping(tokenSign.getValue(), loginId, context.getAe());
        // 保存刷新Token和登录ID的映射
        saveRefreshTokenToIdMapping(tokenSign.getRefreshToken(), loginId, context.getRe());

        context.setSt(tokenSign.getSsoToken());
        context.setRt(tokenSign.getRefreshToken());
        context.setAct(System.currentTimeMillis() / 1000 + context.getAe());
        getSaTokenDao().set(ctxKey, JSON.toJSONString(context), context.getRe());
    }

    /**
     * 通过刷新令牌来重新刷新登录令牌
     *
     * @param refreshToken 刷新令牌
     */
    public OidcToken refreshLogin(String refreshToken, SaSessionProxy sessionProxy, boolean newToken) {
        AuthorContext context = null;
        SaTokenSign tokenSign = null;
        SaSessionProxy session = sessionProxy;
        String loginId = loginIdFromRefreshToken(refreshToken);
        if (StringTool.isNotBlank(loginId)) {
            if (null == session) {
                session = querySessionByLoginId(loginId);
            }

            if (null == session) {
                // 会话已经注销，这种是异常情况
                // 删除刷新令牌到登录ID的映射
                deleteRefreshTokenToIdMapping(refreshToken);
            } else {
                tokenSign = session.tokenSignFromRefreshToken(refreshToken);
                if (null == tokenSign) {
                    // 会话中已经不存在此登录信息了
                    deleteRefreshTokenToIdMapping(refreshToken);
                } else {
                    String oldToken = tokenSign.getValue();
                    Object id = session.getLoginId();
                    context = loginContextFromAccessToken(oldToken);

                    deleteTokenToIdMapping(oldToken);

                    /*
                     * 登录状态设置为可用
                     */
                    tokenSign.setState(0);
                    tokenSign.setRefreshToken(refreshToken);
                    tokenSign.updateExpiredTime(context.getAe(), context.getRe());

                    /*
                     * 重新生产新的访问令牌
                     * 刷新令牌不用重新生成
                     */
                    if (newToken) {
                        tokenSign.setValue(createToken(loginId, "access_token",
                                context.getTer(), context.getRe()));
                    } else {
                        tokenSign.setValue(oldToken);
                    }

                    context.setSt(tokenSign.getSsoToken());
                    context.setRt(refreshToken);
                    context.setLrt(System.currentTimeMillis() / 1000);

                    updateTokenCache(tokenSign, context, id);

                    session.refreshLogin(oldToken, tokenSign);
                }
            }
        } else {
            // 登录已经完全过期
        }

        return (null == tokenSign ? null : TokenUtil.fromTokenSign(tokenSign.getValue(), context));
    }

    /**
     * 在指定的会话上登出指定的登录
     *
     * @param session     会话
     * @param accessToken 访问令牌
     */
    public void logoutOnSession(SaSessionProxy session, String accessToken) {
        if (null != session) {
            int ssdCount = 0;
            boolean needUpdate = false;
            List<TokenSign> removeTokens = new ArrayList<>();

            // todo licode 这里可以考虑使用会话ID来进行分布式上锁

            for (TokenSign tokenSign : session.tokenSignListCopy()) {
                SaTokenSign saTokenSign = (SaTokenSign) tokenSign;
                String loginId = loginIdFromAccessToken(tokenSign.getValue());
                if (SaFoxUtil.isEmpty(loginId)) {
                    removeTokens.add(tokenSign);
                } else {
                    if (SaFoxUtil.equals(tokenSign.getValue(), accessToken)) {
                        removeTokens.add(tokenSign);
                    } else if (StringTool.isNotBlank(saTokenSign.getSsoToken())) {
                        ++ssdCount;
                    }
                }
            }

            /*
             * 在当前会话找到指定的登录
             */
            if (!removeTokens.isEmpty()) {
                removeTokens.forEach(item -> session.removeTokenSign(item, false));
                needUpdate = true;
            }

            /*
             * 当前没有单点登录的用户，清除会话上的单点登录令牌
             */
            if (0 == ssdCount) {
                clearSsoToken(session);
                needUpdate = true;
            }

            if (needUpdate) {
                session.update();
            }

            session.logoutByTokenSignCountToZero();
        }
    }

    /**
     * 清除用户会话上的单点登录信息
     *
     * @param session 用户会话
     */
    private void clearSsoToken(SaSessionProxy session) {
        if (null != session && null != session.getSsoToken()) {
            if (StringTool.isNotBlank(session.getSsoToken().getSsoToken())) {
                deleteSsoTokenToIdMapping(session.getSsoToken().getSsoToken());
            }
            session.clearSsoToken();
        }
    }

    /**
     * 检查当前会话上的所有登录是否超时，如果超时进行清理
     *
     * @param session 会话对象
     */
    public void checkLoginTimeoutOnSession(SaSessionProxy session) {
        if (null != session) {
            int ssdCount = 0;
            boolean needUpdate = false;
            List<TokenSign> expTokens = new ArrayList<>();

            for (TokenSign tokenSign : session.tokenSignListCopy()) {
                SaTokenSign saTokenSign = (SaTokenSign) tokenSign;
                String loginId = loginIdFromRefreshToken(saTokenSign.getRefreshToken());
                if (StringTool.isBlank(loginId)) {
                    expTokens.add(tokenSign);
                    needUpdate = true;
                } else {
                    loginId = loginIdFromAccessToken(saTokenSign.getValue());
                    /*
                     * 登录已经超时
                     */
                    if (StringTool.isBlank(loginId)) {
                        /*
                         * 把登录状态改为等待续期
                         */
                        saTokenSign.setState(1);
                        needUpdate = true;
                    }

                    if (StringTool.isNotBlank(saTokenSign.getSsoToken())) {
                        ++ssdCount;
                    }
                }
            }

            if (0 == ssdCount) {
                session.assignSsoToken(null, 0);
                needUpdate = true;
            }

            if (!expTokens.isEmpty()) {
                expTokens.forEach((item) -> session.removeTokenSign(item, false));
            }

            if (needUpdate) {
                session.update();
            }

            session.logoutByTokenSignCountToZero();
        }
    }

    /**
     * 更具授权上下文，创建登录令牌
     *
     * @param context 授权上下文
     * @return 登录令牌对象
     */
    public SaTokenSign buildOauthTokenSign(AuthorContext context) {
        SaTokenSign tokenSign;
        String device = context.getTer();
        String loginId = context.calcLoginId();
        String accessToken;
        String refreshToken;

        // 校正参数
        if (null == context.getAe() ||
                context.getAe() <= 0) {
            context.setAe(getConfigOrGlobal().getTimeout());
        }

        if (null == context.getRe() ||
                context.getRe() <= 0) {
            context.setRe(context.getAe() * 2);
        }

        accessToken = createToken(loginId, "access_token",
                device, context.getRe());
        refreshToken = createToken(loginId, "refresh_token",
                device, context.getRe());

        tokenSign = new SaTokenSign(accessToken, device, "");
        tokenSign.setRefreshToken(refreshToken);

        return tokenSign;
    }

    public OidcToken tokenInfoFromAccessToken(String accessToken, SaSessionProxy sessionProxy) {
        OidcToken oidcToken;
        SaSessionProxy session = sessionProxy;
        if (null == session) {
            session = querySessionByAccessToken(accessToken);
        }
        if (null == session) {
            return null;
        } else {
            oidcToken = session.tokenInfoFromAccessToken(accessToken);
            return oidcToken;
        }
    }

    public OidcToken tokenInfoFromRefreshToken(String refreshToken) {
        SaSessionProxy session;
        session = querySessionByRefreshToken(refreshToken);
        return (null == session ? null : session.tokenInfoFromRefreshToken(refreshToken));
    }

    public SaSessionProxy querySessionBySsoToken(String ssoToken) {
        String loginId = loginIdFromSsoToken(ssoToken);
        return this.querySessionByLoginId(loginId);
    }

    public SaSessionProxy querySessionByAccessToken(String accessToken) {
        String loginId = loginIdFromAccessToken(accessToken);
        return this.querySessionByLoginId(loginId);
    }

    public SaSessionProxy querySessionByRefreshToken(String refreshToken) {
        String loginId = loginIdFromRefreshToken(refreshToken);
        return this.querySessionByLoginId(loginId);
    }

    public String loginIdFromRefreshToken(String refreshToken) {
        String key = splicingKeyRefreshTokenValue(refreshToken);
        return getSaTokenDao().get(key);
    }

    public String loginIdFromSsoToken(String ssoToken) {
        String key = splicingKeySsoTokenValue(ssoToken);
        return getSaTokenDao().get(key);
    }

    public String loginIdFromAccessToken(String accessToken) {
        String key = splicingKeyTokenValue(accessToken);
        return getSaTokenDao().get(key);
    }

    public AuthorContext loginContextFromAccessToken(String accessToken) {
        String data = getSaTokenDao().get(splicingKeyTokenInfoValue(accessToken));
        return null == data ? null : JSON.parseObject(data, AuthorContext.class);
    }

    public String accessTokenFromRefreshToken(String refreshToken) {
        OidcToken tokenInfo = null;
        SaSessionProxy session;
        session = querySessionByRefreshToken(refreshToken);
        if (null != session) {
            tokenInfo = session.tokenInfoFromRefreshToken(refreshToken);
        }
        return (null == tokenInfo ? "" : tokenInfo.getAccessToken());
    }

    public String refreshTokenFromAccessToken(String accessToken) {
        OidcToken tokenInfo = null;
        SaSessionProxy session;
        session = querySessionByAccessToken(accessToken);
        if (null != session) {
            tokenInfo = session.tokenInfoFromAccessToken(accessToken);
        }
        return (null == tokenInfo ? "" : tokenInfo.getRefreshToken());
    }

    public String splicingKeyTokenInfoValue(String tokenValue) {
        return getConfigOrGlobal().getTokenName() + ":" + loginType + ":access-info:" + tokenValue;
    }

    protected void authLogin(SaSession session, SaLoginModel loginModel,
                             SaTokenSign tokenSign, AuthorContext context) {
        Object id = session.getLoginId();
        String tokenValue = tokenSign.getValue();

        session.addTokenSign(tokenSign);

        // 2、写入这个 token 的最后活跃时间 token-last-active
        if (isOpenCheckActiveTimeout()) {
            setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig());
        }

        tokenSign.updateCreateTime();
        tokenSign.updateUpdateTime();
        context.setLrt(System.currentTimeMillis() / 1000);
        tokenSign.updateExpiredTime(context.getAe(), context.getRe());

        // 2、保存 token -> id 的映射关系，方便日后根据 token 找账号 id
        updateTokenCache(tokenSign, context, id);

        // 4、$$ 发布全局事件：账号 xxx 登录成功
        SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
    }

    public void saveSsoTokenToIdMapping(String tokenValue, Object loginId, long timeout) {
        getSaTokenDao().set(splicingKeySsoTokenValue(tokenValue), String.valueOf(loginId), timeout);
    }

    public void saveAccessTokenToIdMapping(String tokenValue, Object loginId, long timeout) {
        getSaTokenDao().set(splicingKeyAccessTokenValue(tokenValue), String.valueOf(loginId), timeout);
    }

    public void saveRefreshTokenToIdMapping(String tokenValue, Object loginId, long timeout) {
        getSaTokenDao().set(splicingKeyRefreshTokenValue(tokenValue), String.valueOf(loginId), timeout);
    }

    @Override
    public void deleteTokenToIdMapping(String tokenValue) {
        super.deleteTokenToIdMapping(tokenValue);
    }

    public void deleteRefreshTokenToIdMapping(String tokenValue) {
        getSaTokenDao().delete(splicingKeyRefreshTokenValue(tokenValue));
    }

    public void deleteAccessInfo(String tokenValue) {
        getSaTokenDao().delete(splicingKeyTokenInfoValue(tokenValue));
    }

    public String splicingKeySsoTokenValue(String tokenValue) {
        return getConfigOrGlobal().getTokenName() + ":" + loginType + ":sso-token:" + tokenValue;
    }

    public void deleteSsoTokenToIdMapping(String tokenValue) {
        getSaTokenDao().delete(splicingKeySsoTokenValue(tokenValue));
    }

    public String splicingKeyAccessTokenValue(String tokenValue) {
        return getConfigOrGlobal().getTokenName() + ":" + loginType + ":access-token:" + tokenValue;
    }

    public String splicingKeyRefreshTokenValue(String tokenValue) {
        return getConfigOrGlobal().getTokenName() + ":" + loginType + ":refresh-token:" + tokenValue;
    }

    protected String buildUsableToken(Object id, SaLoginModel loginModel, boolean needToken) {
        // 1、如果调用者预定了要生成的 token，则直接返回这个预定的值，框架无需再操心了
        if (needToken && SaFoxUtil.isNotEmpty(loginModel.getToken())) {
            return loginModel.getToken();
        }

        configLoginModel(loginModel);

        // 2、如果代码走到此处，说明未能成功复用旧 token，需要根据算法新建 token
        return SaStrategy.instance.generateUniqueToken.execute(
                "token",
                getConfigOfMaxTryTimes(),
                () -> createTokenValue(id, loginModel.getDeviceOrDefault(),
                        loginModel.getTimeout(), loginModel.getExtraData()),
                tokenValue -> getLoginIdNotHandle(tokenValue) == null
        );
    }

    protected String createToken(Object id, String nodeName, SaLoginModel loginModel) {
        // 1、如果调用者预定了要生成的 token，则直接返回这个预定的值，框架无需再操心了
        if (SaFoxUtil.isNotEmpty(loginModel.getToken())) {
            return loginModel.getToken();
        }

        configLoginModel(loginModel);

        // 2、如果代码走到此处，说明未能成功复用旧 token，需要根据算法新建 token
        return createToken(id, nodeName, loginModel.getDeviceOrDefault(),
                loginModel.getTimeout());
    }

    protected String createToken(Object id, String nodeName, String device, Long timeout) {
        return SaStrategy.instance.generateUniqueToken.execute(
                nodeName,
                getConfigOfMaxTryTimes(),
                () -> createTokenValue(id, device, timeout, null),
                tokenValue -> getLoginIdNotHandle(tokenValue) == null);
    }

    protected SaTokenConfig configLoginModel(SaLoginModel loginModel) {
        SaTokenConfig config = getConfigOrGlobal();
        loginModel.build(config);
        return config;
    }
}
